using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Reflection;
using UDS.Management.Data.Package;
using System.Data.SqlClient;

namespace UDS.Management.Data.Deployment
{
    /// <summary>
    /// Manages updating multiple databases across multiple servers
    /// </summary>
    public class Layout
    {
        [Flags]
        public enum UpdateMode
        {
            None = 0,
            Update = 1,
            Cleanup = 2,
            All = -1
        };

        private const int PROCEDURE_NOT_FOUND_ERROR_CODE = 2812;
        private const int DATABASE_NOT_ENABLED_FOR_REPLICATION = 14013;

        private ServerLayoutHelper _slHelper;
        private Database.DatabaseUpdateDelegate _dbDelegate;

        /// <summary>
        /// Get or set the name of the slave DB.
        /// </summary>
        public string SlaveName
        {
            get;
            set;
        }

        public Layout(ServerLayoutHelper slHelper)
        {
            _slHelper = slHelper;
            _dbDelegate = new Database.DatabaseUpdateDelegate(db_OnUpdate);
        }

        public void Update(DatabaseData[] desiredDatabases, UpdateMode mode, string snapshotFolder, string reportingStoreUncPath, bool allowReinstall)
        {
            if (desiredDatabases == null || desiredDatabases.Length == 0)
            {
                throw new ArgumentNullException("desiredDatabases", "No databases were specified to be installed");
            }
            // =======================================
            // Validate the configuration
            // This will throw a validation exception if there is an error.
            // If no errors, then the list of desired and existing database
            // deployment information is returned in the deployment info object.
            // =======================================            
            Dictionary<string, DatabaseData> desiredDBs = Validate(desiredDatabases);

            if ((mode & UpdateMode.Update) != 0)
            {
                Update(desiredDBs, snapshotFolder, reportingStoreUncPath, allowReinstall);
            }
            if ((mode & UpdateMode.Cleanup) != 0)
            {
                Cleanup(desiredDBs);
            }
        }

        private void Cleanup(Dictionary<string, DatabaseData> desiredDBs)
        {
            // Process each desired database in order
            foreach (DatabaseData deploymentInfo in desiredDBs.Values)
            {
                Database db = deploymentInfo.Database;
                if (deploymentInfo.Package.NeedsCleanup)
                {
                    // Hook up the logging event
                    db.OnUpdate += _dbDelegate;

                    try
                    {
                        if (!db.Exists())
                        {
                            throw new ValidationException(String.Format("The database {0} cannot be found", db.DatabaseName));
                        }
                        this.RaiseMessage("Cleaning Package '{0}' on database {1}", deploymentInfo.Package.GetPackageName(), db);
                        if (!String.IsNullOrEmpty(deploymentInfo.Package.GetPackageDescription()))
                        {
                            this.RaiseMessage("Description: {0}", deploymentInfo.Package.GetPackageDescription());
                        }
                        // Allow cleanups to be re-run
                        db.UpdateDatabase(deploymentInfo.Package, Database.DatabaseUpdateContext.Cleanup, true);
                    }
                    finally
                    {
                        db.OnUpdate -= _dbDelegate;
                    }
                }
            }

        }

        private const string IptvVersion1_1_SP2 = "1.1 SP2";

        private static string GetDisplayVersion(string version)
        {
            if (string.Compare(version, "1.2") == 0)
            {
                version = IptvVersion1_1_SP2;
            }
            return version;
        }

        public static void SubscribeSlaveDB(Database publisherDatabase, string slaveServer, string slaveName, string slaveDatabaseName, ref string version)
        {
            Guid serviceGroupId = Guid.Empty;

            string cmdUpdateInsert = string.Empty;

            if (!publisherDatabase.Exists())
            {
                throw new Exceptions.DatabaseManagementException(String.Format("Database {0} does not exist.", publisherDatabase));
            }

            //ServiceGroup[] sgs = publisherDatabase.GetServiceGroups();
            //if (sgs != null && sgs.Length > 0)
            //{
            //    foreach (ServiceGroup sg in sgs)
            //    {
            //        if (sg.Name == slaveName)
            //        {
            //            serviceGroupId = sg.Id;
            //            cmdUpdateInsert = string.Format(string.Format("exec usp_update_servicegroup_V_2 @serviceGroupId = '{0}', @name = '{1}', @databaseServer = '{2}', @ServiceGroupVersion = '{3}', @description = null, @bootstrapurl = null, @migrationstatus = 0, @BranchFacadeVersion = '{4}'", serviceGroupId, slaveName, slaveServer, version, BranchFacadeVersion));
            //            break;
            //        }
            //    }
            //}
            //if (cmdUpdateInsert.Length == 0)
            //{
            //    serviceGroupId = Guid.NewGuid();
            //    cmdUpdateInsert = string.Format(string.Format("exec usp_insert_servicegroup_V_2 @serviceGroupId = '{0}', @name = '{1}', @databaseServer = '{2}', @ServiceGroupVersion = '{3}', @description = null, @bootstrapurl = null, @migrationstatus = 0, @BranchFacadeVersion = '{4}' ", serviceGroupId, slaveName, slaveServer, version, BranchFacadeVersion));
            //}
            publisherDatabase.RunScript(cmdUpdateInsert);
            publisherDatabase.SubscribeReplication(slaveServer);
            version = GetDisplayVersion(version);
        }

        public static Dictionary<string, IDBPackage> LoadDatabaseDataFromAssembly(PackageLoader packageloader, Assembly assembly)
        {
            Dictionary<string, IDBPackage> packageList = new Dictionary<string, IDBPackage>();

            packageloader.LoadPackages(assembly);
            foreach (string packageName in packageloader.GetPackageNames())
            {
                IDBPackage package = packageloader.GetPackage(packageName);
                packageList.Add(package.GetDatabaseName(), package);
            }
            // Make sure the packages are returned in the proper publisher/subscriber order
            return packageList;
        }

        public event UDS.Management.Data.Deployment.VersionExceptionEventHandler VersionError;
        public event UDS.Management.Data.Database.DatabaseUpdateDelegate LogEvent;

        // ======================================================
        // Private implementation code
        // ======================================================

        private void OnVersionException(VersionException exception)
        {
            VersionExceptionEventArgs Args = new VersionExceptionEventArgs(exception, true);

            if (VersionError != null)
            {
                VersionError(null, Args);
            }

            // If cancel is set, cancel the installation, and throw an exception
            if (Args.Cancel) throw exception;
        }

        /// <summary>
        /// unpublish for the branch/unsubscribe for the SG, and return true only when the branch is unpublished
        /// </summary>
        private bool UpdateReplicationBeforeInstall(Replication replicationInfo, DatabaseData DeploymentInfo)
        {
            try
            {
                Database db = null;
                Database PublisherDB = null;
                string NetBiosName = null;
                if (replicationInfo.Type == ReplicationType.Publisher)
                {
                    db = _slHelper.GetMainDatabase(DeploymentInfo.Package.GetDatabaseName());
                    PublisherDB = db;
                }

                if (replicationInfo.Type == ReplicationType.Subscriber)
                {
                    string PublisherDatabaseName = replicationInfo.DatabaseName;
                    PublisherDB = _slHelper.GetMainDatabase(PublisherDatabaseName);
                    if (PublisherDB == null || !PublisherDB.Exists())
                    {
                        throw new Exceptions.DatabaseManagementException(String.Format("Required publication database {0} not found for subscriber database", PublisherDatabaseName));
                    }
                    NetBiosName = PublisherDB.GetNetBiosServerName();
                    db = new Database(NetBiosName, DeploymentInfo.Package.GetDatabaseName());
                }

                bool replicationAffected = false;
                using (SqlConnection Connection = new SqlConnection(db.ConnectionString))
                {
                    Connection.Open();

                    IEnumerable<SqlSchemaChangeFamily> schemaEnumerable = DeploymentInfo.Package.GetSchemaChanges();
                    Dictionary<Guid, SchemaManagement.SchemaChange> CurrentChanges = UDS.Management.Data.SchemaManagement.Schema.GetAppliedChanges(Connection, db.CommandTimeOut);

                    foreach (SqlSchemaChangeFamily Family in schemaEnumerable)
                    {
                        if (Family.AffectsReplication == true)
                        {
                            replicationAffected = true;
                            break;
                        }
                    }
                }

                if (replicationAffected == true)
                {
                    if (replicationInfo.Type == ReplicationType.Publisher)
                    {
                        DeploymentInfo.Database.UnpublishReplication();
                        return true;
                    }
                    else if (replicationInfo.Type == ReplicationType.Subscriber)
                    {
                        try
                        {
                            DeploymentInfo.Database.UnsubscribeReplication(null);
                            return false;
                        }
                        catch (System.Data.SqlClient.SqlException SqlEx)
                        {
                            if (SqlEx.Number != PROCEDURE_NOT_FOUND_ERROR_CODE && SqlEx.Number != DATABASE_NOT_ENABLED_FOR_REPLICATION)
                            {
                                throw;
                            }
                        }
                    }
                }
            }
            catch (Exception)
            {
            }
            return false;
        }

        private void UpdateReplicationAfterInstall(Replication replicationInfo, DatabaseData DeploymentInfo, Dictionary<string, DatabaseData> DesiredDatabases, string SnapshotDirectory)
        {
            if (replicationInfo.Type == ReplicationType.Subscriber)
            {
                if (string.IsNullOrEmpty(SlaveName))
                    throw new Exceptions.DatabaseManagementException("Missing the name of the slave DB.");

                // Get the name of the database it is subscribed to
                string publisherDatabaseName = replicationInfo.DatabaseName;

                // Get the publisher database from either the installed list of database
                // or from the list of databases being installed
                Database PublisherDB = null;

                if (DesiredDatabases.ContainsKey(publisherDatabaseName))
                {
                    DatabaseData PublisherInfo = DesiredDatabases[publisherDatabaseName];
                    PublisherDB = PublisherInfo.Database;
                }
                else
                {
                    PublisherDB = _slHelper.GetMainDatabase(publisherDatabaseName);
                    if (PublisherDB == null || !PublisherDB.Exists())
                    {
                        // This will get caught by validation, so this code should not
                        // actually ever run - but just in case...
                        throw new Exceptions.DatabaseManagementException(
                            String.Format("Required database {0} not found for database {1}", publisherDatabaseName, DeploymentInfo.Database)
                        );
                    }
                }

                // Get the database object for that subscription db
                Database SubscriberDB = DeploymentInfo.Database;

                string SubscriberNetBiosName = SubscriberDB.GetNetBiosServerName();
                string slaveVersion = replicationInfo.Version; //manifest version
                this.RaiseMessage("Registering {0} with {1} with version {2}", SubscriberDB.DatabaseName, publisherDatabaseName, slaveVersion);
                PublisherDB.RegisterSlaveDB(SlaveName, SubscriberNetBiosName, slaveVersion);

                //get from sgname & version manifest, branchid bm_branch and sgid from bm_servicegroup from branch db and set databasevsersion columns in sgdb
                SlaveDBRegistration serviceGroupRegistration = PublisherDB.GetSlaveDBRegistration(SlaveName);
                if (serviceGroupRegistration == null)
                {
                    throw new Exception(string.Format("Registration had failed for servicegroup {0}", SlaveName));
                }
                SubscriberDB.SetSlaveVersion(serviceGroupRegistration, slaveVersion);

                // Now subscriber to the publisher
                this.RaiseMessage("Subscribing database {0} on server {1} to publication database {2} on server {3}", SubscriberDB.DatabaseName, SubscriberNetBiosName, publisherDatabaseName, PublisherDB.GetNetBiosServerName());

                // Subscribe this database to the publisher
                PublisherDB.SubscribeReplication(SubscriberNetBiosName);
            }
            else if (replicationInfo.Type == ReplicationType.Publisher)
            {
                // Do this everytime, regardless of whether or not the database exists
                Database db = DeploymentInfo.Database;

                this.RaiseMessage("Installing replication for database {0}", db);

                // If this database was newly created - install replication
                db.InstallReplication(SnapshotDirectory);

                this.RaiseMessage("Publishing database");

                // If this is a brand new database, and it is a publisher
                // then run the publication script
                db.PublishReplication();
            }
        }

        private void Update(Dictionary<string, DatabaseData> desiredDatabases, string snapshotDirectory, string reportingStoreDTSLocation, bool allowReinstall)
        {
            // Make sure the snapshot folder exists
            if (!String.IsNullOrEmpty(snapshotDirectory)
                && !System.IO.Directory.Exists(snapshotDirectory))
            {
                try
                {
                    System.IO.Directory.CreateDirectory(snapshotDirectory);
                }
                catch (Exception ex)
                {
                    Exceptions.DatabaseManagementException dbmEx = new UDS.Management.Data.Exceptions.DatabaseManagementException(String.Format("Could not create snap shot folder '{0}'.  {1}", snapshotDirectory, ex.Message));
                    throw dbmEx;
                }
            }

            bool branchUnpublished = false;
            // Process each desired database in order
            foreach (DatabaseData DeploymentInfo in desiredDatabases.Values)
            {
                // Check if the database exists or not - this is used to remember whether or not
                // the publish will need to be run later if the database is a publisher
                Database db = DeploymentInfo.Database;
                // Hook up the logging event
                db.OnUpdate += _dbDelegate;

                try
                {
                    if (db.Exists())
                    {
                        this.RaiseMessage("Updating database {0}", db.DatabaseName);
                    }
                    else
                    {
                        this.RaiseMessage("Installing database {0}", db.DatabaseName);
                        // Create the database 
                        db.CreateDatabase(db.DatabaseName);
                    }

                    // Check if the replication information for this database
                    Replication replicationInfo = DeploymentInfo.Package.Manifest.Replication;

                    if (replicationInfo != null)
                    {
                        branchUnpublished = UpdateReplicationBeforeInstall(replicationInfo, DeploymentInfo);
                    }

                    // Update the database
                    UpdateDatabase(DeploymentInfo, allowReinstall);

                    if (replicationInfo != null)
                    {
                        UpdateReplicationAfterInstall(replicationInfo, DeploymentInfo, desiredDatabases, snapshotDirectory);
                    }
                }
                finally
                {
                    db.OnUpdate -= _dbDelegate;
                }
            }
            if (branchUnpublished)
            {
                ResubscribeAllServiceGroups(desiredDatabases);
            }
        }

        private void ResubscribeAllServiceGroups(Dictionary<string, DatabaseData> DesiredDatabases)
        {
            foreach (DatabaseData DeploymentInfo in DesiredDatabases.Values)
            {
                if (DeploymentInfo.Package.GetDatabaseName() == "MasterDB")
                {
                    throw new NotImplementedException("to do...");
#if false
                    Database PublisherDB = slHelper.GetBranchDatabase(branchName, DeploymentInfo.Package.GetDatabaseName());
                    this.RaiseMessage("Re-Subscribing all ServiceGroup databases on server {0} to branch database", PublisherDB.GetNetBiosServerName());
                    PublisherDB.ReSubscribeAll();
#endif
                }
            }
        }

        private void DeleteTempDir(string tempDir)
        {
            if (tempDir != null && tempDir.Length > 0)
            {
                try
                {
                    Directory.Delete(tempDir, true);
                }
                catch (Exception e)
                {
                    this.RaiseMessage(string.Format("Warning: Error while deleting {0}. {1}", tempDir, e.Message));
                }
            }
        }

        private void RaiseMessage(string format, params object[] args)
        {
            this.db_OnUpdate(this, new Database.DatabaseUpdateEvent(Database.DatabaseUpdateContext.Information, String.Format(format, args)));
        }

        /// <summary>
        /// Update the specified database.
        /// </summary>
        private void UpdateDatabase(DatabaseData data, bool allowReinstall)
        {
            Database db = data.Database;

            // set default ANSI etc. options, and compat level to 90
            db.SetDatabaseOption("ANSI_NULLS", true);
            db.SetDatabaseOption("ANSI_PADDING", true);
            db.SetDatabaseOption("ANSI_WARNINGS", true);
            db.SetDatabaseOption("ARITHABORT", true);
            db.SetDatabaseOption("CONCAT_NULL_YIELDS_NULL", true);
            db.SetDatabaseOption("QUOTED_IDENTIFIER", true);
            db.SetDatabaseOption("NUMERIC_ROUNDABORT", false);

            //bool DatabaseUnpublished = false;

            this.RaiseMessage("Applying Package '{0}'", data.Package.GetPackageName());
            if (!String.IsNullOrEmpty(data.Package.GetPackageDescription()))
            {
                this.RaiseMessage("Description: {0}", data.Package.GetPackageDescription());
            }

            // Run the scripts to update the database
            db.UpdateDatabase(data.Package, allowReinstall);
        }

        void db_OnUpdate(object sender, Database.DatabaseUpdateEvent update)
        {
            if (this.LogEvent != null)
            {
                LogEvent(sender, update);
            }
        }

        /// <summary>
        /// Make sure the configuration is valid, and if it is, return the deployment information.
        /// </summary>
        /// <exception cref="ValidationException">Thrown if the desired deployment has validation errors</exception>
        /// <param name="DesiredDatabases">List of databases to be installed</param>
        /// <returns>A deployment info object with hashed desired and existing database information</returns>
        private Dictionary<string, DatabaseData> Validate(
            DatabaseData[] DesiredDatabases)
        {
            Dictionary<string, DatabaseData> DesiredHash = new Dictionary<string, DatabaseData>();

            foreach (DatabaseData Info in DesiredDatabases)
            {
                // Hash by logical name
                DesiredHash.Add(Info.Package.GetDatabaseName(), Info);
            }

            // Verify that the server exists by connecting to the master database
            List<string> ValidateServerList = new List<string>();

            foreach (DatabaseData Info in DesiredDatabases)
            {
                // If the server hasn't already been checked
                if (!ValidateServerList.Contains(Info.Database.ServerName))
                {
                    // Ping the database
                    PingDatabase(Info.Database.ServerName);

                    // Check the version
                    try
                    {
                        PreRequisiteCheck.CheckSQLServerVersion(Info.Database);
                    }
                    catch (VersionException vEx)
                    {
                        // The user of the library may choose to ignore this exception
                        // and continue.  So fire the event, and the function will throw 
                        // if the user hasn't canceled it.
                        this.RaiseMessage(vEx.Message);
                        OnVersionException(vEx);
                    }

                    // Check if the server collation is equal to SQL_Latin1_General_CP1_CI_AS
                    if (Environment.GetEnvironmentVariable("SKIP_COLLATION_CHECK") != "yes")
                        PreRequisiteCheck.CheckServerCollation(Info.Database.ServerName);

                    // Set server-wide default user connection options:
                    Info.Database.RunScriptAgainstMaster("exec sp_configure 'user options', 4472  reconfigure");

                    ValidateServerList.Add(Info.Database.ServerName);
                }

                // If database exists then its collation must match with server collation
                if (Info.Database.Exists() && Environment.GetEnvironmentVariable("SKIP_COLLATION_CHECK") != "yes")
                    PreRequisiteCheck.CheckCollationMatch(Info.Database);
            }


            // Foreach of the databases to be installed, if it is a subscriber, verify that the
            // publisher is either also being installed, or is already installed
            foreach (DatabaseData Database in DesiredDatabases)
            {
                Replication replicationInfo = Database.Package.Manifest.Replication;
                if (replicationInfo != null &&
                    replicationInfo.Type == ReplicationType.Subscriber)
                {
                    string LogicalPublisherDatabase = replicationInfo.DatabaseName;

                    Database Publisher = null;

                    if (String.IsNullOrEmpty(LogicalPublisherDatabase))
                    {
                        throw new ValidationException(
                            String.Format("No publisher database was specified for subscriber database {0}", Database.Package.GetDatabaseName())
                        );
                    }

                    if (DesiredHash.ContainsKey(LogicalPublisherDatabase))
                    {
                        Publisher = DesiredHash[LogicalPublisherDatabase].Database;
                    }
                    else
                    {
                        // Get the publisher database
                        Publisher = _slHelper.GetMainDatabase(LogicalPublisherDatabase);

                        // Verify that the database really exists
                        if (Publisher == null || !Publisher.Exists())
                        {
                            throw new ValidationException(
                                String.Format(
                                "The publisher database {0} for role {1} doesn't exist for subscriber database {2}",
                                Publisher.DatabaseName,
                                LogicalPublisherDatabase,
                                Database.Package.GetDatabaseName()
                                )
                            );
                        }
                    }

                    // Make sure the database exists
                    if (Publisher == null)
                    {
                        throw new ValidationException(
                            String.Format(
                            "The publisher database {0} was not specified for subscriber database {1}",
                            LogicalPublisherDatabase,
                            Database.Package.GetDatabaseName()
                            )
                        );
                    }

                    // Make sure the server is reachable
                    PingDatabase(Publisher.ServerName);
                    PingDatabase(Database.Database.ServerName);

                    // The subscriber and publisher dbs may not exist at this time, so to get their collations we use their corresponding 
                    // server collations. Remember we have already checked that their collations match with that of their corresponding servers,
                    // if dbs exist.
                    Database subscriberDBServer = new Database(Database.Database.ServerName);
                    string subscriberCollation = subscriberDBServer.GetServerCollation();

                    Database publisherDBServer = new Database(Publisher.ServerName);
                    string publisherCollation = publisherDBServer.GetServerCollation();


                    // Publisher and subscriber database collation must match
                    if (publisherCollation != subscriberCollation)
                    {
                        throw new ValidationException(
                            String.Format(
                            "The collation {0} in publisher database {1} and collation {2} in subsriber database {3} must match",
                            publisherCollation,
                            Publisher.DatabaseName,
                            subscriberCollation,
                            Database.Package.GetDatabaseName()
                            )
                        );
                    }

                    // Make sure the link server is setup correctly from the publisher to the
                    // the subscriber database.  This will throw a validation exception if there
                    // is a problem connecting to the link server.
                    PreRequisiteCheck.CheckLinkedServer(Publisher.ServerName, Database.Database.ServerName);
                }
            }

            // Make sure the databases are in the correct order
            return OrderDatabases(DesiredHash);
        }

        private static Dictionary<string, DatabaseData> OrderDatabases(Dictionary<string, DatabaseData> desiredHash)
        {
            Dictionary<string, DatabaseData> orderedDesiredHash = new Dictionary<string, DatabaseData>();

            foreach (string logicalName in desiredHash.Keys)
            {
                if (!orderedDesiredHash.ContainsKey(logicalName))
                {
                    DatabaseData dbData = desiredHash[logicalName];

                    // If the database is a subscriber, and the hash doesn't already contain its
                    // publisher, then put the publisher in first.
                    global::Replication replInfo = dbData.Package.Manifest.Replication;

                    if (replInfo != null
                        && replInfo.Type == ReplicationType.Subscriber
                        && !orderedDesiredHash.ContainsKey(replInfo.DatabaseName))
                    {

                        string publisherName = replInfo.DatabaseName;

                        if (desiredHash.ContainsKey(publisherName))
                        {
                            DatabaseData publisherData = desiredHash[publisherName];
                            orderedDesiredHash.Add(replInfo.DatabaseName, publisherData);
                        }
                    }

                    orderedDesiredHash.Add(logicalName, dbData);
                }
            }

            return orderedDesiredHash;
        }

        private static void PingDatabase(string serverName)
        {
            Database db = new Database(serverName);
            try
            {
                // Ping the server to make sure it is reachable
                db.Ping();
            }
            catch (Exception e)
            {
                throw new ValidationException(e.Message, e);
            }
        }
    }
}
